<?php
/**
 * Created by PhpStorm.
 * User: lhl
 * Date: 2020/11/30
 * Time: 10:50
 * Features: 公共Trait
 */

namespace app\common\traits;


use app\common\exception\CustomException;
use app\asmpasync\socket\SocketClient;
use app\asmpasync\socket\SocketPackage;
use Directive\Directive;
use think\facade\Config;
use think\facade\Request;
use think\Db;
use think\Exception;

Trait CommonServiceTrait
{
    //状态码
    public $CODE_SUCCESS = 200;
    public $CODE_NO_PARAM = 201;
    public $CODE_FAIL = 202;
    private $response = [];
    private $fail_code = 0;

    /**
     * 查询数据库全名称
     * @param string $db 数据库标识 master为主数据库
    */
    public function getDatabaseName($db = 'master'){
        if($db == 'master'){
            $database = Config::get('database.database');
        }else{
            $database = Config::get("database.{$db}.database");
        }
        return $database;
    }

    /**
     * 查询表前缀
     * @param string $db 数据库标识 master为主数据库
    */
    public function getTablePrefix($db = 'master'){
        if($db == 'master'){
            $prefix = Config::get('database.prefix');
        }else{
            $prefix = Config::get("database.{$db}.prefix");
        }
        return $prefix;
    }

    /**
     * 跨库join查询,处理表名
     * @param string $table     表名,不包含前缀!
     * @param string $db        数据库标识 master为主数据库
    */
    public function getChildDatabaseFullTableName($table,$db = 'master'){
        //数据库名称
        $database = $this->getDatabaseName($db);
        //表前缀
        $prefix = $this->getTablePrefix($db);
        //完整表名 = 数据库名称 + 表前缀 + 原表名
        return $database . '.'. $prefix . $table;
    }

    /**
     * 同步下发命令至客户端
     * @param string $module           模块标识
     * @param string $command          命令
     * @param bool   $isCallBack       是否有回调接口,不要修改默认值
     * @param bool   $isSystem         是否为系统下发,系统下发时,用户id默认为0
     * @param array  $hostIds          需要指定下发的主机id，数组格式，范围必须是后台操作的主机中的子集
     *
     * @return mixed array|bool
     * 返回值为false时: 所有主机没有进行下发操作(并非下发失败)
     * 返回值为数组时:  每台主机的发送结果
     */
    public function sendCommandToClient($module,$command,$isCallBack = true,$isSystem = false,array $hostIds = []){
        set_time_limit(0);
        //后台操作失败,则不下发
        global $errCode;
        if ($errCode != $this->CODE_SUCCESS)  return false;

        //当前用户id 如果是系统下发 则为0
        $userId = $isSystem ? 0 : $this->getLoginUserId();
        //当前操作主机列表
        $hosts = $this->getHost('','array',$hostIds,false);

        //如果排除后没有主机,则直接返回
        if(!$hosts) return false;

        $hostIds = array_column($hosts,'id');
        //验证模块,模块名称统一为小写
        $config = $this->checkCommand($module, $command);

        //命令描述
        $description = $config['description'].':'.$config['commands'][$command]['description'];
        //查询IP和端口
        $hostLink = Db::name('host_link')->whereIn('host_id', $hostIds)->where('module', $module)->select();

        if(!$hostLink){
            throw new CustomException('下发命令至客户端失败：主机连接信息不存在', $this->fail_code);
        }

        //发送结果
        $result = [];
        //循环发送命令至每一台主机的该模块
        foreach ($hosts as $host){
            //当前主机的连接信息
            $host['link'] = null;
            foreach ($hostLink as $link){
                //匹配连接信息,匹配不到,则该主机发送失败
                if($host['id'] == $link['host_id']){
                    $host['link'] = [
                        'link_id' => $link['id'], //连接id,用于后续可能修改主机在线状态
                        'ip' => $link['ip'],
                        'port' => $link['port'],
                        'module' => $link['module'],
                        'host_id' => $link['host_id']
                    ];
                    break;
                }
            }

            //没有连接信息的,直接失败
            if(!$host['link']){
                $result[] = [
                    'host_id' => $host['id'],                                           //主机id
                    'host_name' => $host['name'],                                      //主机名称
                    'host_alias' => $host['alias'],                                    //主机别名
                    'result' => false,                                                  //发送结果
                    'errorMsg' => '下发命令至客户端失败：该主机的连接信息不存在',    //失败原因
                    'content' => $description                                           //操作内容
                ];
                //不进行后续操作
                continue;
            }

            Db::startTrans();
            try{
                //第一次写入空数据
                $messageData = [
                    'host_id' => $host['link']['host_id'], //主机id
                    'user_id' => $userId,                    //用户id
                    'module' =>  $host['link']['module'],   //模块
                    'ip' =>  $host['link']['ip'],           //IP地址
                    'port' =>  $host['link']['port'],           //端口
                    'description' => $description, //描述
                    'ctime' => time(),
                    'callback_status' => $isCallBack ? 0 : 1, //回调状态
                    'content' => '',
                    'content_md5' => '',
                ];
                $logId = Db::name('server_socket_message_history')->insertGetId($messageData);
                if(!$logId){
                    Db::rollback();
                    $result[] = [
                        'host_id' => $host['id'],                                           //主机id
                        'host_name' => $host['name'],                                      //主机名称
                        'host_alias' => $host['alias'],                                    //主机别名
                        'result' => false,                                                  //发送结果
                        'errorMsg' => '下发命令至客户端失败：保存数据失败',              //失败原因
                        'content' => $description                                           //操作内容
                    ];
                    //不进行后续操作
                    continue;
                }
                //组装buffer并发送消息到客户端 获取发送返回信息
                $messageData =  $this->sendToClient($host['link'], $command, $logId);

                Db::name('server_socket_message_history')->where('id',$logId)->update($messageData);
                //提交事务
                Db::commit();
                //成功数据
                $result[] = [
                    'host_id' => $host['id'],                                           //主机id
                    'host_name' => $host['name'],                                      //主机名称
                    'host_alias' => $host['alias'],                                    //主机别名
                    'result' => $this->response['status'],                            //发送结果
                    'errorMsg' => $this->response['status'] ? '下发命令成功' : '下发命令至客户端失败：' . $this->response['message'],              //失败原因
                    'content' => $description                                           //操作内容
                ];

            }catch (Exception $e){
                Db::rollback();
                $result[] = [
                    'host_id' => $host['id'],                                           //主机id
                    'host_name' => $host['name'],                                      //主机名称
                    'host_alias' => $host['alias'],                                    //主机别名
                    'result' => false,                                                  //发送结果
                    'errorMsg' => '下发命令至客户端失败：' . $e->getMessage(),        //失败原因
                    'content' => $description                                            //操作内容
                ];
                //不进行后续操作
                continue;
            }

            //删除多余变量
            unset($format);
            unset($socketPackage);
            unset($socketClient);

        }

        //将结果作为全局变量传递给jsonOut方法
        global $directiveResult;
        //合并之前的结果
        $directiveResult = $directiveResult ? $directiveResult : [];

        $directiveResult = array_merge($directiveResult,$result);

        return $result;
    }

    /**
     * @name: 调用异步
     * @Param:
     * @Description:
     * @Author: foo
     * @Date: 2019-11-29 13:34:54
     */
    public function Async($module, $method, $parmas =[])
    {
        global $errCode;
        if ($errCode != $this->CODE_SUCCESS)
        {
            return false;
        }
        $user = $this->getUserInfo();
        $url = request()->domain() . '/async/v3/callClient';
        $data = [
            'module' => $module,
            'action' => $method,
            'params' => json_encode($parmas),
            'user_id' => $user['id'],
            'host_id' => $this->getHost('id', 'string'),
        ];

        curlPost($url, $data, 2);
    }

    /**
     * 获取主机信息 (数组)
     * @param string $field             查询字段
     * @param string $type              返回数据格式
     * @param array  $hostIds           需要指定的主机id数组，必须为整个主机列表的子集
     * @param bool   $isThrowException  排除后,主机为空，是否抛出异常，默认为true
     */
    public function getHost($field = '', $type = 'array',array $hostIds = [],$isThrowException = true)
    {
        //获取后台勾选操作的所有的主机列表
        global $online_host ;
        if(!$online_host){
            throw new CustomException('未获取任何主机信息',422);
        }
        //筛选主机
        if($hostIds){
            //需要返回的主机列表
            $return = [];
            foreach ($online_host as $host){
                if(in_array($host['id'],$hostIds)){
                    $return[] = $host;
                }
            }

            //如果筛选后,主机为空,是否抛出异常
            if (empty($return) && $isThrowException)
            {
                throw new CustomException('未获取任何主机信息',422);
            }

        }else{
            $return = $online_host;
        }

        //不为空,才进行数据格式处理
        if($return){
            if(!empty($field))
            {
                $return = array_column($return, $field, 'id');
            }

            if ($type == 'string')
            {
                $return = implode(',', $return);
            }
        }else{
            if(!empty($field))
            {
                return [];
            }
            if ($type == 'string')
            {
                return '';
            }
        }
        return $return;
    }

    /**
     * @name 获取当前用户信息
     * @return mixed
     * @author wx
     * @date 2019/11/4 16:09
     */
    public function getUserInfo()
    {
        $userInfo['id'] = Request::param('user_id');
        $userInfo['level'] = Request::param('user_level');
        if(!$userInfo['id']){
            $userInfo['id'] = 1;
            $userInfo['real_name'] = '管理平台';
            $userInfo['level'] = 1;
        }

        return $userInfo;
    }

    /**
     *  回调
     */
    public function toDo ($action, $params = [], $return = [])
    {
        if ($this->getErrorCode() != $this->CODE_SUCCESS)
        {
            $this->setError($this->getErrorCode(), $this->getErrorMsg());
            return false;
        }

        $returnClassMethod = is_array($action) ? $action : [$this, $action];
        if (!call_user_func_array('method_exists', $returnClassMethod))
        {
            $this->setError($this->CODE_FAIL, '不存在这个方法');
            return $return;
        }
        return call_user_func_array($returnClassMethod, $params);
    }

    /**
     * @name: 主机输出语句
     * @Param:
     * @Description:
     * @Author: foo
     * @Date: 2019-12-24 15:35:57
     */
    public function hostErrorMessage ($host_id_a, $message)
    {
        $host_a = $this->getHost();
        $host_name_a = [];
        foreach ($host_id_a as $host_id)
        {
            !isset($host_a[$host_id]) ? : $host_name_a[] = $host_a[$host_id]['name'] ;
        }

        $return =  '主机' . '['. implode(']，[', $host_name_a) . ']' . $message;

        return $return;
    }

    /**
     * @name: 设置错误信息
     * @Param:
     * @Description:
     * @Author: foo
     * @Date: 2019-05-22 16:44:38
     */
    public function setError ($code, $msg)
    {
        global $errCode;
        $errCode = $code;
        global $errMsg;
        $errMsg = $msg;
    }

    /**
     * @name: 获取错误码
     * @Param:
     * @Description:
     * @Author: foo
     * @Date: 2019-05-22 16:44:18
     */
    public function getErrorCode ()
    {
        global $errCode;
        return $errCode;
    }

    /**
     * @name: 获取错误信息
     * @Param:
     * @Description:
     * @Author: foo
     * @Date: 2019-05-22 16:43:48
     */
    public function getErrorMsg ()
    {
        global $errMsg;
        return $errMsg;
    }

    /**
     * @name: 发送消息到客户端
     * @Param: 
     * @Description: 
     * @Author: foo
     * @Date: 2020-12-14 14:23:27
     */
    public function sendToClient ($link, $command, $logId = 0)
    {
        //组装protobuf格式的数据
        $format = new Directive();
        //设置数据 命令字符串
        $format->setCommand($command);
        //设置日志id
        $format->setLogId($logId);
        //打包数据
        $socketPackage = new SocketPackage();
        $socketPackage->host_id = $link['host_id'];//主机id
        $socketPackage->module = $link['module'];//模块
        $socketPackage->sync = false;//是否客户端返回
        //打包数据
        $data = $socketPackage->dataPackage($format);
        //发送数据
        $socketClient = new SocketClient($link['ip'],$link['port']);
        //响应数据
        $this->response = $socketClient->sendMsg($data);
        //如果下发失败且是socket错误，则将对应主机的在线状态重置为离线
        if(!$this->response['status'] && $this->response['is_socket_error']){
            //将主机状态修改为离线
            Db::name('host_link')->where('id',$link['link_id'])->setField('status',0);
        }

        //socket错误信息
        $socketError = [
            'err_id' => $socketClient->error_id,
            'err_msg' => $socketClient->err_msg,
        ];

        //错误id:优先以socket错误为准
        if($socketError['err_id'] > 0){
            $errId = $socketError['err_id'];
            //错误信息
            $errMsg = trim($socketError['err_msg']);
        }elseif (!$this->response['status'] && !$this->response['is_socket_error']){
            //如果失败且不是socket错误,则固定错误id
            $errId = 401;
            //错误信息
            $errMsg = trim($socketError['err_msg'] . ' ' . $this->response['message']);
        }else{
            //如果成功errid = 0
            $errId = 0;
            //错误信息
            $errMsg = trim($socketError['err_msg'] . ' ' . $this->response['message']);
        }

        //更新日志数据
        $base64Data = base64_encode($data);
        $messageData = [
            'content' => $base64Data,
            'content_md5' => md5($base64Data),
            'err_id' => $errId,
            'err_msg' => $errMsg,
        ];

        return $messageData;
    }


    /**
     * @name: 检查指令
     * @Param: 
     * @Description: 
     * @Author: foo
     * @Date: 2020-12-14 14:39:11
     */
    public function checkCommand (&$module, $command)
    {
        //验证模块,模块名称统一为小写
        $module = strtolower($module);
        $this->fail_code = Config::get('exception.api_status_code.directive_error',504);

        $config = Config::get('directive.'.$module);

        if(!$config || !isset($config['description'])){
            throw new CustomException('下发命令至客户端失败：模块未配置', $this->fail_code);
        }
        //验证指令
        if(!isset($config['commands'][$command]) || !isset($config['commands'][$command]['description'])){
            throw new CustomException('下发命令至客户端失败：命令未配置', $this->fail_code);
        }

        return $config;
    }
}